<?php
/**
 * Project:     drylamp - rapid application development framework for php
 * File:        ActiveRecord.php
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 *
 * @copyright 2009-2010 Ilya St.
 * @author Ilya St. <provided at gmail dot com>
 * @version 1.0
 */

//TODO enable ability to merge 2 or more data objects into 1 to be able to perform all ActiveRecord operations on a single object
//     rather than on bunch of separate ones

abstract class ActiveRecord implements  SetGetAble, Exposable  {
	
    protected $mTableName;
    public static $mVerbose = false;
    public static $mEOL = "\n";
    
    public $mFields = array();
    protected $mPriKey;
    protected $mFieldsWrappers = array(); // TODO finish this functionality

    /**
     * Array of names of fields, that can be returned by getFields().
     * If null - all fields will be returned. 
     * @var array
     */
    protected $mExposableFields = null;
    
    private static $describeCache;
    
	public function __construct() {
	    $tableName = strtolower(get_class($this));
		  $this->say("setting table name: " . $tableName); 
	    $this->mTableName = $tableName;
	    $this->loadDescribe();
	}
	
	
	/**
	 * Loads definition of table or view
	 *
	 */ 
	public function loadDescribe() {
		$descrCacheKey = Conf::get('DB_NAME')."::".$this->getTableName();
		if (is_array(self::$describeCache) && array_key_exists($descrCacheKey, self::$describeCache) ) {
			$this->mFields = self::$describeCache[$descrCacheKey]['fields'];
			$this->mPriKey = self::$describeCache[$descrCacheKey]['pri_key'];
			return;
		}
	  $describeSql = "DESCRIBE " . $this->mTableName;
	  $rows = DBAdapter::queryAssoc($describeSql);
	  
	  if (is_array($rows)) foreach( $rows as $key => $value ) {
	    $this->mFields[$value['Field']] =  array(  
	                                                       'Field'=> $value['Field'],        
	                                                       'value'=>NULL, 
	                                                       'type' => $value['Type'],  
	                                                       'null'=>(strtolower($value['Null'])=="yes")?TRUE:FALSE, 
	                                                       'key' => $value['Key'],
	                                                       'default' => $value['Default'],
	                                                       'extra'   => $value['Extra'],
	    																									 'isset'	=> false   
	                                                     );         
	                             
	    if ( $value['Key'] == "PRI" )  $this->mPriKey = $value['Field']; 
	  }
	  self::$describeCache[$descrCacheKey]['fields']  = $this->mFields;
	  self::$describeCache[$descrCacheKey]['pri_key'] = $this->mPriKey;
	} 
	
	
	public function __get($name) {
	  return $this->mFields[$name]['value'];
	}
	
	
	public function __set($name, $value) {
		$this->mFields[$name]['isset'] = true;
	  return $this->mFields[$name]['value'] = $value;
	}
	
	protected function say($param) {
	  if (self::$mVerbose) echo $param . self::$mEOL;
	}
	
	public function load($pParam) {
	  $getSQL = "SELECT * FROM " . $this->mTableName . " WHERE " . $this->mPriKey . "=" . $this->validateByType($this->mFields[$this->mPriKey]['type'], $pParam, true) . " LIMIT 1";
	  $result = DBAdapter::queryAssoc($getSQL);
	  if (is_null($result)) return FALSE;
	  foreach ($result[0] as $field => $value ) {
	    $this->$field = $value;
	  }
	  return TRUE;
	}
	
	public function update() {
	  $updSQL = "UPDATE " . $this->mTableName .  " SET ";
      foreach( $this->mFields as $name => $field ) {
	    if ( $name == $this->mPriKey ) continue;
      //make sure we update only intended fields:
	    if ($field['isset']) $updSQL .=  $name. "='". $this->validateByType($field['type'],$field['value']) . "' , ";
	  }
	  $updSQL = substr($updSQL, 0, -2);
	  
	  if ($this->mFields[$this->mPriKey]['isset'] == false) {
	  	throw new ActiveRecordException("Can't update - primary key value is not set");
	  }
	  $updSQL .= " WHERE " . $this->mPriKey . "='" .$this->validateByType($this->mFields[$this->mPriKey]['type'],$this->mFields[$this->mPriKey]['value'])."' LIMIT 1";
	  return DBAdapter::queryAffectedRows($updSQL);
	}
	
	
	/**
	 * Inserts a new record into the table. If true is passed as a param - will do REPLACE INTO instead.
	 * @param boolean $pReplace
	 * @return int
	 */
	public function insert($pReplace = FALSE) {
	  $insertSQL = (($pReplace)?"REPLACE":"INSERT") . " INTO ". $this->mTableName ;
	  $names = ""; $values = "";
	  foreach( $this->mFields as $name => $field ) {
	    $names  .= $name. ",";
	    $values .= "'".$this->validateByType($field['type'],$field['value']) . "',";
	  }
	  $names = " (" . substr($names, 0,-1).") ";
	  $values = " (" . substr($values, 0, -1).") ";
	  $insertSQL .= $names. " VALUES " . $values;
	  return DBAdapter::queryLastInsertId($insertSQL);
	}
	
	public function get($pUseNulls = TRUE) {
	  $getSQL = "SELECT * FROM " . $this->mTableName . " WHERE ";
	  $getSQL .= $this->buildWhereClause($pUseNulls);
	  $result = DBAdapter::queryAssoc($getSQL);
	  
	  if (is_array($result)) {
        return $this->assocArrResToObjArr($result);
	  } 
	  return NULL;
	}
	
	
	/**
	 * Deletes the row(s) by primary key, LIMIT is optional
	 *
	 * @return unknown
	 */
	public function delete($pLimit = -1) {
		if ($this->mFields[$this->mPriKey]['isset'] == false) {
			throw new ActiveRecordException("Can't delete, in order to be deleted, object should have priKey set");
		}
	  $delSQL = "DELETE  FROM " . $this->mTableName . " WHERE " . $this->mPriKey . "='".$this->mFields[$this->mPriKey]['value']."'";
	  if ( $pLimit != -1 ) $delSQL .= " LIMIT " . (int)$pLimit;
	  $result = DBAdapter::query($delSQL, DBAdapter::AFFECTED_ROWS);
	  //return $result; //TODO: roll out in next release
	  return NULL;
	}
	
	public function validateByType($pField, $pValue, $pWrapInQutes = false) {
	  if ( is_null($pValue) ) return "NULL";
	  elseif (is_bool($pValue)) {
	  	if ($pValue == true) return "TRUE";
	  	else return "FALSE";
	  }
	  elseif(is_numeric($pValue)) {
	  	return $pValue;
	  }
	  if ($pWrapInQutes) return  "'".DBAdapter::escapeString($pValue)."'" ;
	  return  DBAdapter::escapeString($pValue) ;   
	}
	
	
	protected function buildWhereClause($pUseNulls = TRUE) {
	  $pLogicOP = " AND ";
	  $columns = "";
	  foreach( $this->mFields as $name => $field ) {
	  	//TODO: rewrite this using isset field
	    if ( $pUseNulls == false && is_null($field['value']) ) continue;  
	    $columns .= $this->mTableName.".".$name. "=". $this->validateByType($field['type'],$field['value'], true) . $pLogicOP;
	  }
	  return substr($columns,0, -1* strlen($pLogicOP));
	}
	
	/**
	 * Creates an array of data objects out of associative array
	 * $pExplicitClassName is needed to handle cases when this method called from OnFly class
	 * 
	 * @param array $result
	 * @param string $pExplicitClassName
	 * @return unknown
	 */
	public function assocArrResToObjArr(Array $result, $pExplicitClassName = NULL) {
	  	$className = get_class($this);
        $retArr = array();	    
	    foreach ($result as $row) {
	      $obj = new $className($pExplicitClassName);
	      //$obj->loadFromAssoc($row);  //TODO: roll this out in next release
	      
	      foreach( $row as $name=>$value )  {
	        if (isset($this->mFields[$name])) $obj->$name = $value;  // assign values of fields:
	          //else throw new ActiveRecordResultSetInconsistency("Result set contains a field, which is not a field of " . $this->mTableName);
	      } 
	         
	      $retArr[] = clone($obj) ;                                 // TODO: test and try without clone()
	    }	    
	    return $retArr;
	}
	
	
	public function loadFromAssoc( Array $pArray ) {
	  foreach ($this->mFields as $key => $value) {
	    if (isset($pArray[$key])) $this->$key = $pArray[$key];
	  }
	}
	
	/**
	 * Returns exposable fields, or if $this->mExposableFields is null - all fields
	 * @see scaffold/Exposable#getFields()
	 */
	public function getFields() {
	  $retArr = array();
	  if (is_null($this->mExposableFields)) {
			$retArr = $this->getAllFields();
	  }
	   else {
	     foreach( $this->mFields as $key=>$value ) {
	       if (in_array($key, $this->mExposableFields)) $retArr[$key] = $value['value']; 
	     }
	   }
	  return $retArr;
	}
	
	/**
	 * returns all fields
	 * @return array
	 */
	public function getAllFields() {
	  $retArr = array();
		foreach( $this->mFields as $key=>$value ) {
	    $retArr[$key] = $value['value']; 
	  }
	  return $retArr;
	}
	
	public function getPriKeyVal() {
		return $this->mFields[$this->mPriKey]['value'];
	}
	
	public function getPriKey() {
		return $this->mPriKey;
	}
	
	/**
	 * Decides whether to put "'"'s around 
	 * @param $pKeyName string
	 * @return string
	 */
	public function prepareKeyVal($pKeyName) {
		return $this->validateByType($pKeyName, $this->mFields[$pKeyName]['value'], true);
	}
	
	public function getTableName() {
		return $this->mTableName;
	}
	
	/**
	 * Compares $this object with any other ActiveRecord object.
	 * 2 objects are considered equal if they have same table names AND same number of exposable fields AND same values of 
	 * corresponding exposable fields
	 * @param $o
	 * @return boolean
	 */
	public function equals(ActiveRecord $o) {
		if (strcasecmp($o->getTableName(), $this->getTableName()) != 0 ) return false;
		$o_fields = $o->getFields();
		$this_fields = $this->getFields();
    
		if (count($o_fields) != count($this_fields)) return false;
		
		foreach($o_fields as $key => $value) {
			if ( (array_key_exists($key, $this_fields) && $o_fields[$key] == $this_fields[$key]) == false ) return false;
		}
		return true;
	}
	
	
	/**
	 * Produces list of fields in the format table_name.field, separated by comma and space
	 * @return string
	 */
	public function getFieldsListStr() {
		$fields = $this->getAllFields();
		$str = "";
		$delim = ", ";
		foreach( $fields as $key => $f ) {
			$str .= $this->mTableName . "." . $key . $delim;
		}
		return substr($str, 0, -1 * strlen($delim));
	}
	
	
	//not tested
	public function isFieldSet($pField) {
		return $this->mFields[$pField]['isset'];
	}
	
}


class ActiveRecordException extends Exception {}
class ActiveRecordResultSetInconsistency extends ActiveRecordException {};

